package com.mazaiting.aoplog.aop;

import com.alibaba.fastjson.JSONObject;
import com.mazaiting.aoplog.domain.entity.SysAccessLog;
import com.mazaiting.aoplog.service.ISysAccessLogService;
import com.mazaiting.common.core.utils.StringUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.time.LocalDateTime;

/**
 * Log 日志拦截
 *
 * @author mazaiting
 * @create_time 2022/3/28 09:53
 */
@Aspect
@Order(2)
@Component
public class LogAspect {

    /**
     * 切入点
     */
    private static final String POINT_CUT = "execution(* com.mazaiting.*.controller.*Controller.*(..))";

    /**
     * 日志工具
     */
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 切入点
     */
    @Pointcut(POINT_CUT)
    private void pointCut() {
    }

    /**
     * 每次日志插入的全局Id
     * 使用 ThreadLocal 解决多线程问题
     */
    private final ThreadLocal<Long> accessLogIdThreadLocal = new ThreadLocal<>();

    /**
     * 插入日志服务
     */
    @Resource
    private ISysAccessLogService sysAccessLogService;

    /**
     * 在切点之前执行
     *
     * @param joinPoint 切入点
     */
    @Before(value = "pointCut()")
    public void allBefore(JoinPoint joinPoint) {
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String methodName = joinPoint.getSignature().getName();
        StringBuilder sb = new StringBuilder();
        //日志打印
        sb.append("请求信息: ").append(className).append(".").append(methodName).append(", 参数params:");
        // 参数遍历
        Object[] args = joinPoint.getArgs();
        sb.append(this.parseParams(args));
        logger.info(sb.toString());
    }

    /**
     * 返回之后调用
     *
     * @param returnObj 返回对象
     */
    @AfterReturning(value = "pointCut()", returning = "returnObj")
    public void afterReturn(JoinPoint joinPoint, Object returnObj) {
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String methodName = joinPoint.getSignature().getName();
        StringBuilder sb = new StringBuilder();
        sb.append("响应信息: ").append(className).append(".").append(methodName);
        if (returnObj != null) {
            String result = JSONObject.toJSONString(returnObj);
            sb.append(", 响应: ").append(result);
            if (!(returnObj instanceof File)) {
                SysAccessLog sysAccessLog = sysAccessLogService.getById(getAccessLogId());
                if (StringUtils.hasLength(result) && result.length() <= 1000) {
                    sysAccessLog.setResult(result);
                }
                sysAccessLogService.updateById(sysAccessLog);
            }

        }
        // 获取信息
        logger.info(sb.toString());
    }

    /**
     * 抛异常进入
     *
     * @param e 异常信息
     */
    @AfterThrowing(value = "pointCut()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Throwable e) {
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String methodName = joinPoint.getSignature().getName();
        String errMsg = className + "." + methodName + ", 异常信息: " + e.getMessage();
        logger.error("异常: {}", errMsg);
        // 执行异常更新
        // 创建AR模式
        SysAccessLog sysAccessLog = new SysAccessLog();
        // 查询刚刚存入的id
        sysAccessLog = sysAccessLog.selectById(getAccessLogId());
        // 保存异常信息
        if (StringUtils.hasLength(e.getMessage()) && e.getMessage().length() < 1000) {
            sysAccessLog.setExceptionType(e.getMessage());
        } else {
            sysAccessLog.setExceptionType("异常信息为空或异常信息过长请查看日志");
        }
        // 更新数据
        sysAccessLogService.updateById(sysAccessLog);
    }

    /**
     * 环绕通知
     *
     * @param joinPoint 切入点
     * @return 返回信息
     * @throws Throwable 异常
     */
    @Around(value = "pointCut()")
    public Object allAround(ProceedingJoinPoint joinPoint) throws Throwable {
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        Long begin = System.currentTimeMillis();
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        StringBuilder sb = new StringBuilder();
        Object result;
        // 创建实体assess记录实体
        SysAccessLog sysAccessLog = new SysAccessLog();
        // 访问的类以及方法
        sysAccessLog.setUrl(className + "/" + methodName);
        // 设置请求参数
        sysAccessLog.setParam(this.parseParams(args));
        // 获取id地址
        sysAccessLog.setIp(request.getRemoteAddr());
        // 设置访问时间
        sysAccessLog.setAccessTime(LocalDateTime.now());
        // 保存数据
        sysAccessLogService.save(sysAccessLog);
        setAccessLogIdThreadLocal(sysAccessLog.getId());
        try {
            result = joinPoint.proceed();
        } finally {
            Long end = System.currentTimeMillis();
            Long execTime = end - begin;
            sb.append("环绕通知: ").append(className).append(".").append(methodName)
                    .append(", 执行时间: ").append(execTime).append("ms");
            logger.info(sb.toString());
            // 将已经添加的数据进行修改
            sysAccessLog.setExecTime(execTime);
            // 更新数据
            sysAccessLogService.updateById(sysAccessLog);
            setAccessLogIdThreadLocal(null);
        }
        return result;
    }


    /**
     * 解析参数
     *
     * @param args 对象数组
     * @return 字符串参数
     */
    private String parseParams(Object[] args) {
        String[] arguments = new String[args.length];
        for (int i = 0; i < args.length; i++) {
            // 如果是请求, 则获取请求头
            if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) {
                //ServletRequest不能序列化，从入参里排除，否则报异常：java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
                //ServletResponse不能序列化 从入参里排除，否则报异常：java.lang.IllegalStateException: getOutputStream() has already been called for this response
//                String accessToken = HttpHeaderUtil.getAccessToken((HttpServletRequest) args[i]);
                arguments[i] = args[i].toString();
                continue;
            }
            // 设置参数
            arguments[i] = String.valueOf(args[i]);
        }
        return StringUtil.join("{", "}", ",", arguments);
    }

    /**
     * 获取访问日志 ID
     * @return 访问日志 ID
     */
    private long getAccessLogId() {
        return accessLogIdThreadLocal.get();
    }

    /**
     * 访问访问 ID
     * @param accessLogId 访问日志 ID
     */
    private void setAccessLogIdThreadLocal(Long accessLogId) {
        this.accessLogIdThreadLocal.set(accessLogId);
    }
}
